﻿// <copyright file="ModelDataAccess.cs" company="City of Portland, BES-ASM">
// </copyright>
// <summary>ModelDataAccess class</summary>

namespace BESASM.EMGAATS.DataAccess
{
  using System;
  using System.Collections.Generic;
  using System.Data;
  using System.Linq;
  using System.Text;
  using System.Threading.Tasks;
  using BESASM.EMGAATS.Arc;
  using BESASM.EMGAATS.BusinessClasses;
  using ESRI.ArcGIS.Carto;
  using ESRI.ArcGIS.esriSystem;
  using ESRI.ArcGIS.Geodatabase;
  using ESRI.ArcGIS.Geometry;
  using FluentValidation.Results;

  /// <summary>
  /// A class for reading and writing data from a EMGAATS model database
  /// </summary>
  public class ModelDataAccess : INetworkWriter, IArcGISDataSource
  {
    private const string DefaultEditReason = "UPDATED";
    private const string DefaultAppendReason = "APPENDED";
    private const string DefaultDeleteReason = "DELETED";
    private const double DefaultNodeDiameterFt = 4.0;

    private bool schemaError = false;
    private DataSchema schema = new DataSchema();
    private Dictionary<DataColumn, int> fieldIndices = new Dictionary<DataColumn, int>();
    private Dictionary<EntityType, int> nextIds = new Dictionary<EntityType, int>();

    /// <summary>
    /// Initializes a new instance of the <see cref="ModelDataAccess"/> class.
    /// </summary>
    public ModelDataAccess()
    {      
      this.EntityLookup = new Dictionary<EntityType, Dictionary<int, IEntity>>();
      foreach (EntityType entityType in Enum.GetValues(typeof(EntityType)))
      {
        this.EntityLookup[entityType] = new Dictionary<int, IEntity>();
      }

      this.Validation = new ValidationResult();
    }

    #region IArcGISDataSource Members

    /// <summary>
    /// Gets or sets the workspace representing the ArcGIS data source.
    /// </summary>
    public IWorkspace Workspace { get; set; }

    #endregion

    /// <summary>
    /// Gets a dictionary for looking up entities given an ObjectId
    /// </summary>
    public Dictionary<EntityType, Dictionary<int, IEntity>> EntityLookup { get; private set; }

    /// <summary>
    /// Gets validation results for the last load or save operation
    /// </summary>
    public ValidationResult Validation { get; private set; }

    #region INetworkReader Members

    /// <summary> 
    /// Loads the network from the workspace
    /// </summary>
    /// <param name="network">The network to load.</param>
    public void LoadNetwork(BusinessClasses.INetwork network)
    {
      if (network == null)
      {
        throw new ArgumentNullException("network", "The network to load has not been specified.");
      }

      if (this.Workspace == null)
      {
        throw new InvalidOperationException("The workspace to load the network from has not been specified.");
      }

      foreach (EntityType entityType in Enum.GetValues(typeof(EntityType)))
      {
        this.EntityLookup[entityType].Clear();
      }

      this.Validation = new ValidationResult();
      this.schemaError = false;

      IFeatureWorkspace featureWorkspace = (IFeatureWorkspace)this.Workspace;

      this.LoadNodes(featureWorkspace, network);
      this.LoadCrossSections(featureWorkspace, network);
      this.LoadLinks(featureWorkspace, network);
      this.LoadWeirs(featureWorkspace, network);
      this.LoadOrifices(featureWorkspace, network);
      this.LoadAreas(featureWorkspace, network);
      this.LoadDirectors(featureWorkspace, network);
      this.LoadInfiltrators(featureWorkspace, network);
      this.LoadStorages(featureWorkspace, network);

      // Check IsModified
      bool modified = network.Areas.FirstOrDefault(area => area.IsModified) != null;
      modified |= network.CrossSections.FirstOrDefault(crossSection => crossSection.IsModified) != null;
      modified |= network.Directors.FirstOrDefault(director => director.IsModified) != null;
      modified |= network.Links.FirstOrDefault(link => link.IsModified) != null;
      modified |= network.Nodes.FirstOrDefault(node => node.IsModified) != null;
      modified |= network.Orifices.FirstOrDefault(orifice => orifice.IsModified) != null;
      ////modified |= network.Pumps.FirstOrDefault(pump => pump.IsModified) != null;
      modified |= network.Weirs.FirstOrDefault(weir => weir.IsModified) != null;
      modified |= network.Infiltrators.FirstOrDefault(infiltrator => infiltrator.IsModified) != null;
      modified |= network.Storages.FirstOrDefault(storage => storage.IsModified) != null;
      System.Diagnostics.Debug.Assert(!modified, "The newly loaded network has modified entities.");
    }

    #endregion

    #region INetworkWriter Members

    /// <summary>
    /// Creates the database tables and feature classes based on the network data schema
    /// </summary>
    public void CreateNetworkSchema()
    {
      if (this.Workspace == null)
      {
        throw new InvalidOperationException("The workspace to create the network schema in has not been specified.");
      }

      ISpatialReferenceFactory spatialRefFactory = new SpatialReferenceEnvironmentClass();
      ISpatialReference spatialReference = spatialRefFactory.CreateProjectedCoordinateSystem((int)esriSRProjCS4Type.esriSRProjCS_NAD1983HARN_StatePlane_Oregon_North_FIPS_3601_Feet_Intl);
      IFeatureWorkspace workspace = this.Workspace as IFeatureWorkspace;
      IFeatureDataset networkDataset = workspace.CreateFeatureDataset("Network", spatialReference);

      foreach (DataTable tableSchema in this.schema.Tables)
      {
        if (tableSchema.Columns.Contains("Shape"))
        {
          this.CreateFeatureClass(tableSchema, networkDataset);
        }
        else
        {
          this.CreateTable(tableSchema);
        }
      }
    }

    /// <summary>
    /// Saves a network to the workspace.
    /// </summary>
    /// <param name="network">The network to save.</param>
    public void SaveNetwork(BusinessClasses.INetwork network)
    {
      if (network == null)
      {
        throw new ArgumentNullException("network", "The network to save has not been specified.");
      }

      if (this.Workspace == null)
      {
        throw new InvalidOperationException("The workspace to save the network to has not been specified.");
      }

      this.Validation = new ValidationResult();
      this.schemaError = false;

      this.SetNextIds(network);

      IFeatureWorkspace featureWorkspace = (IFeatureWorkspace)this.Workspace;

      this.UpdateNodes(featureWorkspace, network);
      this.UpdateCrossSections(featureWorkspace, network);
      this.UpdateLinks(featureWorkspace, network);
      this.UpdateOrifices(featureWorkspace, network);
      this.UpdateWeirs(featureWorkspace, network);
      this.UpdateDirectors(featureWorkspace, network);
      this.UpdateAreas(featureWorkspace, network);
      this.UpdateInfiltrators(featureWorkspace, network);
      this.UpdateStorages(featureWorkspace, network);
    }

    #endregion

    #region IEntityReader Members

    /// <summary>
    /// Gets the entity specified by querying the database.
    /// </summary>
    /// <param name="entityType">The entity type to get.</param>
    /// <param name="id">The entity id.</param>
    /// <returns>A new entity populated with data from the database.</returns>
    public IEntity GetEntity(EntityType entityType, int id)
    {
      throw new NotImplementedException();
    }

    /// <summary>
    /// Refreshes the properties of the given entity based on the data in the database.
    /// </summary>
    /// <param name="entity">The entity to be refreshed.</param>
    public void RefreshEntity(IEntity entity)
    {
      throw new NotImplementedException();
    }

    /// <summary>
    /// Returns a value indicating whether the specified entity exists in the database.
    /// </summary>
    /// <param name="entityType">The entity type to check.</param>
    /// <param name="id">The entity id.</param>
    /// <returns>A value indicating whether the specified entity exists in the database.</returns>
    public bool EntityExists(EntityType entityType, int id)
    {
      throw new NotImplementedException();
    }

    /// <summary>
    /// Returns a value indicating whether a corresponding record exists for the given entity.
    /// </summary>
    /// <param name="entity">The entity to check.</param>
    /// <returns>A value indicating whether a corresponding record exists for the given entity.</returns>
    public bool EntityExists(IEntity entity)
    {
      throw new NotImplementedException();
    }

    #endregion

    #region IEntityWriter Members

    /// <summary>
    /// Deletes the given entity from the database.
    /// </summary>
    /// <param name="entity">The entity to delete.</param>
    public void DeleteEntity(IEntity entity)
    {
      throw new NotImplementedException();
    }

    /// <summary>
    /// Updates the record corresponding to the given entity with current property values.
    /// </summary>
    /// <param name="entity">The entity to update.</param>
    public void UpdateEntity(IEntity entity)
    {
      throw new NotImplementedException();
    }

    /// <summary>
    /// Adds a corresponding record for the given entity with current property values.
    /// </summary>
    /// <param name="entity">The entity to add.</param>
    /// <remarks>This may update the entity id.</remarks>
    public void AddEntity(IEntity entity)
    {
      throw new NotImplementedException();
    }

    #endregion

    private void SetNextIds(BusinessClasses.INetwork network)
    {
      if (network.Areas.Count > 0)
      {
        this.nextIds[EntityType.Area] = network.Areas.Max(area => area.Id) + 1;
      }

      if (network.CrossSections.Count > 0)
      {
        this.nextIds[EntityType.CrossSection] = network.CrossSections.Max(crossSection => crossSection.Id) + 1;
      }

      if (network.Directors.Count > 0)
      {
        this.nextIds[EntityType.Director] = network.Directors.Max(director => director.Id) + 1;
      }

      if (network.Links.Count > 0)
      {
        this.nextIds[EntityType.Link] = network.Links.Max(link => link.Id) + 1;
      }

      if (network.Nodes.Count > 0)
      {
        this.nextIds[EntityType.Node] = network.Nodes.Max(node => node.Id) + 1;
      }

      if (network.Orifices.Count > 0)
      {
        this.nextIds[EntityType.Orifice] = network.Orifices.Max(orifice => orifice.Id) + 1;
      }

      ////if (network.Pumps.Count > 0)
      ////{
      ////  this.nextIds[EntityType.Pump] = network.Pumps.Max(pump => pump.Id) + 1;
      ////}

      if (network.Weirs.Count > 0)
      {
        this.nextIds[EntityType.Weir] = network.Weirs.Max(weir => weir.Id) + 1;
      }

      List<EntityType> keys = new List<EntityType>(this.nextIds.Keys);
      foreach (EntityType entityType in keys)
      {
        if (this.nextIds[entityType] < 1)
        {
          this.nextIds[entityType] = 1;
        }
      }
    }

    #region Load Classes

    private void LoadNodes(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      IFeatureClass nodeClass = this.CheckFeatureClass(workspace, this.schema.Nodes);

      if (this.schemaError)
      {
        return;
      }

      IFeatureCursor cursor = nodeClass.Search(null, false);
      try
      {
        IFeature nodeFeature = cursor.NextFeature();
        while (nodeFeature != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(nodeFeature.get_Value(this.fieldIndices[this.schema.Nodes.node_idColumn]));
            if (id <= 0)
            {
              string message = string.Format("Node record has invalid {0} (Object ID = {1}).", this.schema.Nodes.node_idColumn.ColumnName, nodeFeature.OID);
              ValidationFailure invalidNodeId = new ValidationFailure(this.schema.Nodes.node_idColumn.ColumnName, message, id);
              this.Validation.Errors.Add(invalidNodeId);
            }
            else
            {
              INode node = network.CreateNode(id);
              this.FillNode(node, nodeFeature);
              network.Add(node);
              node.SetModified(false);
              this.EntityLookup[EntityType.Node].Add(nodeFeature.OID, node);
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(nodeFeature);
          }

          nodeFeature = cursor.NextFeature();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    private void LoadCrossSections(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      ITable crossSectionTable = this.CheckTable(workspace, this.schema.CrossSections);
      ITable dataTable = this.CheckTable(workspace, this.schema.CrossSectionData);

      if (this.schemaError)
      {
        return;
      }

      ICursor cursor = crossSectionTable.Search(null, false);
      try
      {
        IRow crossSectionRow = cursor.NextRow();
        while (crossSectionRow != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(crossSectionRow.get_Value(this.fieldIndices[this.schema.CrossSections.cross_section_idColumn]));
            if (id <= 0)
            {
              string message = string.Format("Cross section record has invalid {0} (Object ID = {1}).", this.schema.CrossSections.cross_section_idColumn.ColumnName, crossSectionRow.OID);
              ValidationFailure invalidCrossSectionId = new ValidationFailure(this.schema.CrossSections.cross_section_idColumn.ColumnName, message, id);
              this.Validation.Errors.Add(invalidCrossSectionId);
            }
            else
            {
              ICrossSection crossSection = network.CreateCrossSection(id);
              this.FillCrossSection(crossSection, crossSectionRow, dataTable);
              network.Add(crossSection);
              crossSection.SetModified(false);
              this.EntityLookup[EntityType.CrossSection].Add(crossSectionRow.OID, crossSection);
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(crossSectionRow);
          }

          crossSectionRow = cursor.NextRow();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      // Check that there are no invalid cross section rows
      cursor = dataTable.Search(null, false);
      try
      {
        HashSet<int> invalidIds = new HashSet<int>();
        IRow dataRow = cursor.NextRow();
        while (dataRow != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(dataRow.get_Value(this.fieldIndices[this.schema.CrossSectionData.cross_section_idColumn]));
            if (!network.CrossSections.Contains(id) && !invalidIds.Contains(id))
            {
              string message = string.Format("Cross section data found for cross section Id {0}, which does not exist in cross section table.", id);
              ValidationFailure orphanCrossSectionData = new ValidationFailure(this.schema.CrossSectionData.cross_section_idColumn.ColumnName, message, id);
              orphanCrossSectionData.CustomState = new ValidationState(id, ErrorLevel.Warning);
              this.Validation.Errors.Add(orphanCrossSectionData);
              invalidIds.Add(id);
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(dataRow);
          }

          dataRow = cursor.NextRow();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    private void LoadLinks(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      IFeatureClass linkClass = this.CheckFeatureClass(workspace, this.schema.Links);
      ITable asBuiltsTable = this.CheckTable(workspace, this.schema.AsBuilts);

      if (this.schemaError)
      {
        return;
      }

      IFeatureCursor cursor = linkClass.Search(null, false);
      try
      {
        IFeature linkFeature = cursor.NextFeature();
        while (linkFeature != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(linkFeature.get_Value(this.fieldIndices[this.schema.Links.link_idColumn]));
            if (id <= 0)
            {
              string message = string.Format("Link record has invalid {0} (Object ID = {1}).", this.schema.Links.link_idColumn.ColumnName, linkFeature.OID);
              ValidationFailure invalidLinkId = new ValidationFailure(this.schema.Links.link_idColumn.ColumnName, message, id);
              this.Validation.Errors.Add(invalidLinkId);
            }
            else
            {
              ILink link = network.CreateLink(id);
              this.FillLink(link, linkFeature, asBuiltsTable, network);
              network.Add(link);
              link.SetModified(false);
              this.EntityLookup[EntityType.Link].Add(linkFeature.OID, link);
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(linkFeature);
          }

          linkFeature = cursor.NextFeature();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    private void LoadOrifices(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      ITable orificeTable = this.CheckTable(workspace, this.schema.Orifices);

      if (this.schemaError)
      {
        return;
      }

      ICursor cursor = orificeTable.Search(null, false);
      try
      {
        IRow orificeRow = cursor.NextRow();
        while (orificeRow != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(orificeRow.get_Value(this.fieldIndices[this.schema.Orifices.orifice_idColumn]));
            if (id <= 0)
            {
              string message = string.Format("Orifice record has invalid {0} (Object ID = {1}).", this.schema.Orifices.orifice_idColumn.ColumnName, orificeRow.OID);
              ValidationFailure invalidOrificeID = new ValidationFailure(this.schema.Orifices.orifice_idColumn.ColumnName, message, id);
              this.Validation.Errors.Add(invalidOrificeID);
            }
            else
            {
              IOrifice orifice = network.CreateOrifice(id);
              this.FillOrifice(orifice, orificeRow, network);
              network.Add(orifice);
              orifice.SetModified(false);
              this.EntityLookup[EntityType.Orifice].Add(orificeRow.OID, orifice);
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(orificeRow);
          }

          orificeRow = cursor.NextRow();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    private void LoadWeirs(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      ITable weirTable = this.CheckTable(workspace, this.schema.Weirs);

      if (this.schemaError)
      {
        return;
      }

      ICursor cursor = weirTable.Search(null, false);
      try
      {
        IRow weirRow = cursor.NextRow();
        while (weirRow != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(weirRow.get_Value(this.fieldIndices[this.schema.Weirs.weir_idColumn]));
            if (id <= 0)
            {
              string message = string.Format("Weir record has invalid {0} (Object ID = {1}).", this.schema.Weirs.weir_idColumn.ColumnName, weirRow.OID);
              ValidationFailure invalidWeirId = new ValidationFailure(this.schema.Weirs.weir_idColumn.ColumnName, message, id);
              this.Validation.Errors.Add(invalidWeirId);
            }
            else
            {
              IWeir weir = network.CreateWeir(id);
              this.FillWeir(weir, weirRow, network);
              network.Add(weir);
              weir.SetModified(false);
              this.EntityLookup[EntityType.Weir].Add(weirRow.OID, weir);
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(weirRow);
          }

          weirRow = cursor.NextRow();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    private void LoadDirectors(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      IFeatureClass directorClass = this.CheckFeatureClass(workspace, this.schema.Directors);

      if (this.schemaError)
      {
        return;
      }

      IFeatureCursor cursor = directorClass.Search(null, false);
      try
      {
        IFeature directorFeature = cursor.NextFeature();
        while (directorFeature != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(directorFeature.get_Value(this.fieldIndices[this.schema.Directors.director_idColumn]));
            if (id <= 0)
            {
              string message = string.Format("Director record has invalid {0} (Object ID = {1}).", this.schema.Directors.director_idColumn.ColumnName, directorFeature.OID);
              ValidationFailure invalidDirectorId = new ValidationFailure(this.schema.Directors.director_idColumn.ColumnName, message, id);
              this.Validation.Errors.Add(invalidDirectorId);
            }
            else
            {
              IDirector director = network.CreateDirector(id);
              this.FillDirector(director, directorFeature, network);
              network.Add(director);
              director.SetModified(false);
              this.EntityLookup[EntityType.Director].Add(directorFeature.OID, director);
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(directorFeature);
          }

          directorFeature = cursor.NextFeature();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    private void LoadAreas(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      IFeatureClass areaClass = this.CheckFeatureClass(workspace, this.schema.Areas);

      if (this.schemaError)
      {
        return;
      }

      IFeatureCursor cursor = areaClass.Search(null, false);
      try
      {
        IFeature areaFeature = cursor.NextFeature();
        while (areaFeature != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(areaFeature.get_Value(this.fieldIndices[this.schema.Areas.area_idColumn]));
            if (id <= 0)
            {
              string message = string.Format("Area record has invalid {0} (Object ID = {1}).", this.schema.Areas.area_idColumn.ColumnName, areaFeature.OID);
              ValidationFailure invalidAreaId = new ValidationFailure(this.schema.Areas.area_idColumn.ColumnName, message, id);
              this.Validation.Errors.Add(invalidAreaId);
            }
            else
            {
              BusinessClasses.IArea area = network.CreateArea(id);
              this.FillArea(area, areaFeature, network);
              network.Add(area);
              area.SetModified(false);
              this.EntityLookup[EntityType.Area].Add(areaFeature.OID, area);
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(areaFeature);
          }

          areaFeature = cursor.NextFeature();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    private void LoadInfiltrators(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      ITable infiltratorTable = this.CheckTable(workspace, this.schema.Infiltrators);

      if (this.schemaError)
      {
        return;
      }

      ICursor cursor = infiltratorTable.Search(null, false);
      try
      {
        IRow infiltratorRow = cursor.NextRow();
        while (infiltratorRow != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(infiltratorRow.get_Value(this.fieldIndices[this.schema.Infiltrators.infiltrator_idColumn]));
            if (id <= 0)
            {
              string message = string.Format("Infiltrator record has invalid {0} (Object ID = {1})", this.schema.Infiltrators.infiltrator_idColumn.ColumnName, infiltratorRow.OID);
              ValidationFailure invalidInfiltratorID = new ValidationFailure(this.schema.Infiltrators.infiltrator_idColumn.ColumnName, message, id);
              this.Validation.Errors.Add(invalidInfiltratorID);
            }
            else
            {
              IInfiltrator infiltrator = network.CreateInfiltrator(id);
              this.FillInfiltrator(infiltrator, infiltratorRow, network);
              infiltrator.SetModified(false);
              network.Add(infiltrator);
              this.EntityLookup[EntityType.Infiltrator].Add(infiltratorRow.OID, infiltrator);
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(infiltratorRow);
          }

          infiltratorRow = cursor.NextRow();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    private void LoadStorages(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      ITable storageTable = this.CheckTable(workspace, this.schema.Storages);

      if (this.schemaError)
      {
        return;
      }

      ICursor cursor = storageTable.Search(null, false);
      try
      {
        IRow storageRow = cursor.NextRow();
        while (storageRow != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(storageRow.get_Value(this.fieldIndices[this.schema.Storages.storage_idColumn]));
            if (id <= 0)
            {
              string message = string.Format("Storage record has invalid {0} (Object ID = {1}).", this.schema.Storages.storage_idColumn.ColumnName, storageRow.OID);
              ValidationFailure invalidStorageID = new ValidationFailure(this.schema.Storages.storage_idColumn.ColumnName, message, id);
              this.Validation.Errors.Add(invalidStorageID);
            }
            else
            {
              IStorage storage = network.CreateStorage(id);
              this.FillStorage(storage, storageRow, network);
              storage.SetModified(false);
              network.Add(storage);
              this.EntityLookup[EntityType.Storage].Add(storageRow.OID, storage);
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(storageRow);
          }

          storageRow = cursor.NextRow();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    #endregion

    #region Update Classes

    private void UpdateNodes(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      IFeatureClass nodeClass = this.CheckFeatureClass(workspace, this.schema.Nodes);

      if (this.schemaError)
      {
        return;
      }

      // First update existing nodes
      HashSet<int> existingFeatures = new HashSet<int>();
      IFeatureCursor cursor = nodeClass.Update(null, false);
      try
      {
        IFeature nodeFeature = cursor.NextFeature();
        while (nodeFeature != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(nodeFeature.get_Value(this.fieldIndices[this.schema.Nodes.node_idColumn]));
            if (network.Nodes.Contains(id))
            {
              existingFeatures.Add(id);

              INode node = network.Nodes[id];

              if (node.IsModified)
              {
                this.AuditUpdate(node);
                this.UpdateNodeFeature(nodeFeature, node);
                cursor.UpdateFeature(nodeFeature);
                node.SetModified(false);
              }
            }
            else
            {
              nodeFeature.Delete();
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(nodeFeature);
          }

          nodeFeature = cursor.NextFeature();
        }

        cursor.Flush();
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      // Second, add new nodes
      if (network.Nodes.Count > existingFeatures.Count)
      {
        IFeatureBuffer nodeBuffer = nodeClass.CreateFeatureBuffer();
        cursor = nodeClass.Insert(false);
        try
        {
          foreach (INode node in network.Nodes)
          {
            if (!existingFeatures.Contains(node.Id))
            {
              this.AuditInsert(node);
              this.UpdateNodeFeature(nodeBuffer, node);
              cursor.InsertFeature(nodeBuffer);
              node.SetModified(false);
            }
          }

          cursor.Flush();
        }
        finally
        {
          ComUtility.ReleaseComObject(cursor);
        }
      }
    }

    private void UpdateCrossSections(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      ITable crossSectionTable = this.CheckTable(workspace, this.schema.CrossSections);
      ITable dataTable = this.CheckTable(workspace, this.schema.CrossSectionData);

      if (this.schemaError)
      {
        return;
      }

      // First update existing cross sections
      HashSet<int> existingFeatures = new HashSet<int>();
      ICursor cursor = crossSectionTable.Update(null, false);
      try
      {
        IRow crossSectionRow = cursor.NextRow();
        while (crossSectionRow != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(crossSectionRow.get_Value(this.fieldIndices[this.schema.CrossSections.cross_section_idColumn]));
            if (network.CrossSections.Contains(id))
            {
              existingFeatures.Add(id);

              ICrossSection crossSection = network.CrossSections[id];

              if (crossSection.IsModified)
              {
                // TODO: Consider creating edit operation (make sure it is not nested)
                this.AuditUpdate(crossSection);
                this.UpdateCrossSectionRow(crossSectionRow, crossSection);
                cursor.UpdateRow(crossSectionRow);
                this.UpdateCrossSectionData(dataTable, crossSection, id);
                crossSection.SetModified(false);
              }
            }
            else
            {
              // TODO: Consider creating edit operation (make sure it is not nested)
              crossSectionRow.Delete();
              this.DeleteCrossSectionData(dataTable, id);
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(crossSectionRow);
          }

          crossSectionRow = cursor.NextRow();
        }

        cursor.Flush();
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      // Second, add new cross sections
      if (network.CrossSections.Count > existingFeatures.Count)
      {
        IRowBuffer crossSectionBuffer = crossSectionTable.CreateRowBuffer();
        cursor = crossSectionTable.Insert(false);
        try
        {
          foreach (ICrossSection crossSection in network.CrossSections)
          {
            if (!existingFeatures.Contains(crossSection.Id))
            {
              // TODO: Consider creating edit operation (make sure it is not nested)
              this.AuditInsert(crossSection);
              this.UpdateCrossSectionRow(crossSectionBuffer, crossSection);
              cursor.InsertRow(crossSectionBuffer);
              this.UpdateCrossSectionData(dataTable, crossSection, crossSection.Id);
              crossSection.SetModified(false);
            }
          }
        }
        finally
        {
          ComUtility.ReleaseComObject(cursor);
        }
      }
    }

    private void UpdateLinks(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      IFeatureClass linkClass = this.CheckFeatureClass(workspace, this.schema.Links);
      ITable asBuiltsTable = this.CheckTable(workspace, this.schema.AsBuilts);

      if (this.schemaError)
      {
        return;
      }

      // First update existing links
      HashSet<int> existingFeatures = new HashSet<int>();
      IFeatureCursor cursor = linkClass.Update(null, false);
      try
      {
        IFeature linkFeature = cursor.NextFeature();
        while (linkFeature != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(linkFeature.get_Value(this.fieldIndices[this.schema.Links.link_idColumn]));
            if (network.Links.Contains(id))
            {
              existingFeatures.Add(id);

              ILink link = network.Links[id];

              if (link.IsModified)
              {
                this.AuditUpdate(link);
                this.UpdateLinkFeature(linkFeature, link);
                cursor.UpdateFeature(linkFeature);
                this.UpdateAsBuilts(asBuiltsTable, link, id);
                link.SetModified(false);
              }
            }
            else
            {
              linkFeature.Delete();
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(linkFeature);
          }

          linkFeature = cursor.NextFeature();
        }

        cursor.Flush();
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      // Second, add new links
      if (network.Links.Count > existingFeatures.Count)
      {
        IFeatureBuffer linkBuffer = linkClass.CreateFeatureBuffer();
        cursor = linkClass.Insert(false);
        try
        {
          foreach (ILink link in network.Links)
          {
            if (!existingFeatures.Contains(link.Id))
            {
              this.AuditInsert(link);
              this.UpdateLinkFeature(linkBuffer, link);
              cursor.InsertFeature(linkBuffer);
              this.UpdateAsBuilts(asBuiltsTable, link, link.Id);
              link.SetModified(false);
            }
          }

          cursor.Flush();
        }
        finally
        {
          ComUtility.ReleaseComObject(cursor);
        }
      }
    }

    private void UpdateOrifices(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      ITable orificeTable = this.CheckTable(workspace, this.schema.Orifices);

      if (this.schemaError)
      {
        return;
      }

      HashSet<int> existingFeatures = new HashSet<int>();
      ICursor cursor = orificeTable.Update(null, false);

      try
      {
        IRow orificeRow = cursor.NextRow();
        while (orificeRow != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(orificeRow.get_Value(this.fieldIndices[this.schema.Orifices.orifice_idColumn]));
            if (network.Orifices.Contains(id))
            {
              existingFeatures.Add(id);

              IOrifice orifice = network.Orifices[id];

              if (orifice.IsModified)
              {
                this.AuditUpdate(orifice);
                this.UpdateOrificeRow(orificeRow, orifice);
                cursor.UpdateRow(orificeRow);
                orifice.SetModified(false);
              }
            }
            else
            {
              orificeRow.Delete();
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(orificeRow);
          }

          orificeRow = cursor.NextRow();
        }

        cursor.Flush();
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      // Second, add new orifices
      if (network.Orifices.Count > existingFeatures.Count)
      {
        IRowBuffer orificeBuffer = orificeTable.CreateRowBuffer();
        cursor = orificeTable.Insert(false);
        try
        {
          foreach (IOrifice orifice in network.Orifices)
          {
            if (!existingFeatures.Contains(orifice.Id))
            {
              this.AuditInsert(orifice);
              this.UpdateOrificeRow(orificeBuffer, orifice);
              cursor.InsertRow(orificeBuffer);
              orifice.SetModified(false);
            }
          }

          cursor.Flush();
        }
        finally
        {
          ComUtility.ReleaseComObject(cursor);
        }
      }
    }

    private void UpdateWeirs(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      ITable weirTable = this.CheckTable(workspace, this.schema.Weirs);

      if (this.schemaError)
      {
        return;
      }

      // First update existing links
      HashSet<int> existingFeatures = new HashSet<int>();
      ICursor cursor = weirTable.Update(null, false);
      try
      {
        IRow weirRow = cursor.NextRow();
        while (weirRow != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(weirRow.get_Value(this.fieldIndices[this.schema.Weirs.weir_idColumn]));
            if (network.Weirs.Contains(id))
            {
              existingFeatures.Add(id);

              IWeir weir = network.Weirs[id];

              if (weir.IsModified)
              {
                this.AuditUpdate(weir);
                this.UpdateWeirRow(weirRow, weir);
                cursor.UpdateRow(weirRow);
                weir.SetModified(false);
              }
            }
            else
            {
              weirRow.Delete();
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(weirRow);
          }

          weirRow = cursor.NextRow();
        }

        cursor.Flush();
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      // Second, add new weirs
      if (network.Weirs.Count > existingFeatures.Count)
      {
        IRowBuffer weirBuffer = weirTable.CreateRowBuffer();
        cursor = weirTable.Insert(false);
        try
        {
          foreach (IWeir weir in network.Weirs)
          {
            if (!existingFeatures.Contains(weir.Id))
            {
              this.AuditInsert(weir);
              this.UpdateWeirRow(weirBuffer, weir);
              cursor.InsertRow(weirBuffer);
              weir.SetModified(false);
            }
          }

          cursor.Flush();
        }
        finally
        {
          ComUtility.ReleaseComObject(cursor);
        }
      }
    }

    private void UpdateDirectors(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      IFeatureClass directorClass = this.CheckFeatureClass(workspace, this.schema.Directors);

      if (this.schemaError)
      {
        return;
      }

      // First update existing Directors
      HashSet<int> existingFeatures = new HashSet<int>();
      IFeatureCursor cursor = directorClass.Update(null, false);
      try
      {
        IFeature directorFeature = cursor.NextFeature();
        while (directorFeature != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(directorFeature.get_Value(this.fieldIndices[this.schema.Directors.director_idColumn]));
            if (network.Directors.Contains(id))
            {
              existingFeatures.Add(id);

              IDirector director = network.Directors[id];

              if (director.IsModified)
              {
                this.AuditUpdate(director);
                this.UpdateDirectorFeature(directorFeature, director, network);
                cursor.UpdateFeature(directorFeature);
                director.SetModified(false);
              }
            }
            else
            {
              directorFeature.Delete();
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(directorFeature);
          }

          directorFeature = cursor.NextFeature();
        }

        cursor.Flush();
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      // Second, add new directors
      if (network.Directors.Count > existingFeatures.Count)
      {
        IFeatureBuffer directorBuffer = directorClass.CreateFeatureBuffer();
        cursor = directorClass.Insert(false);
        try
        {
          foreach (IDirector director in network.Directors)
          {
            if (!existingFeatures.Contains(director.Id))
            {
              this.AuditInsert(director);
              this.UpdateDirectorFeature(directorBuffer, director, network);
              cursor.InsertFeature(directorBuffer);
              director.SetModified(false);
            }
          }

          cursor.Flush();
        }
        finally
        {
          ComUtility.ReleaseComObject(cursor);
        }
      }
    }

    private void UpdateAreas(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      IFeatureClass areaClass = this.CheckFeatureClass(workspace, this.schema.Areas);

      if (this.schemaError)
      {
        return;
      }

      // First update existing areas
      HashSet<int> existingFeatures = new HashSet<int>();
      IFeatureCursor cursor = areaClass.Update(null, false);
      try
      {
        IFeature areaFeature = cursor.NextFeature();
        while (areaFeature != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(areaFeature.get_Value(this.fieldIndices[this.schema.Areas.area_idColumn]));
            if (network.Areas.Contains(id))
            {
              existingFeatures.Add(id);

              BusinessClasses.IArea area = network.Areas[id];

              if (area.IsModified)
              {
                this.AuditUpdate(area);
                this.UpdateAreaFeature(areaFeature, area);
                cursor.UpdateFeature(areaFeature);
                area.SetModified(false);
              }
            }
            else
            {
              areaFeature.Delete();
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(areaFeature);
          }

          areaFeature = cursor.NextFeature();
        }

        cursor.Flush();
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      // Second, add new areas
      if (network.Areas.Count > existingFeatures.Count)
      {
        IFeatureBuffer areaBuffer = areaClass.CreateFeatureBuffer();
        cursor = areaClass.Insert(false);
        try
        {
          foreach (BusinessClasses.IArea area in network.Areas)
          {
            if (!existingFeatures.Contains(area.Id))
            {
              this.AuditInsert(area);
              this.UpdateAreaFeature(areaBuffer, area);
              cursor.InsertFeature(areaBuffer);
              area.SetModified(false);
            }
          }

          cursor.Flush();
        }
        finally
        {
          ComUtility.ReleaseComObject(cursor);
        }
      }
    }

    private void UpdateInfiltrators(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      ITable infiltratorTable = this.CheckTable(workspace, this.schema.Infiltrators);

      if (this.schemaError)
      {
        return;
      }

      HashSet<int> existingFeatures = new HashSet<int>();
      ICursor cursor = infiltratorTable.Update(null, false);

      try
      {
        IRow infiltratorRow = cursor.NextRow();
        while (infiltratorRow != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(infiltratorRow.get_Value(this.fieldIndices[this.schema.Infiltrators.infiltrator_idColumn]));
            if (network.Infiltrators.Contains(id))
            {
              existingFeatures.Add(id);

              IInfiltrator infiltrator = network.Infiltrators[id];

              if (infiltrator.IsModified)
              {
                this.AuditUpdate(infiltrator);
                this.UpdateInfiltratorRow(infiltratorRow, infiltrator);
                cursor.UpdateRow(infiltratorRow);
                infiltrator.SetModified(false);
              }
            }
            else
            {
              infiltratorRow.Delete();
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(infiltratorRow);
          }

          infiltratorRow = cursor.NextRow();
        }

        cursor.Flush();
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      // Add new infiltrators
      if (network.Infiltrators.Count > existingFeatures.Count)
      {
        IRowBuffer infiltratorBuffer = infiltratorTable.CreateRowBuffer();
        cursor = infiltratorTable.Insert(false);
        try
        {
          foreach (IInfiltrator infiltrator in network.Infiltrators)
          {
            if (!existingFeatures.Contains(infiltrator.Id))
            {
              this.AuditInsert(infiltrator);
              this.UpdateInfiltratorRow(infiltratorBuffer, infiltrator);
              cursor.InsertRow(infiltratorBuffer);
              infiltrator.SetModified(false);
            }
          }

          cursor.Flush();
        }
        finally
        {
          ComUtility.ReleaseComObject(cursor);
        }
      }
    }

    private void UpdateStorages(IFeatureWorkspace workspace, BusinessClasses.INetwork network)
    {
      ITable storageTable = this.CheckTable(workspace, this.schema.Storages);

      if (this.schemaError)
      {
        return;
      }

      HashSet<int> existingFeatures = new HashSet<int>();
      ICursor cursor = storageTable.Update(null, false);

      try
      {
        IRow storageRow = cursor.NextRow();
        while (storageRow != null)
        {
          try
          {
            int id = DataUtility.SafeConvertInt32(storageRow.get_Value(this.fieldIndices[this.schema.Storages.storage_idColumn]));
            if (network.Storages.Contains(id))
            {
              existingFeatures.Add(id);

              IStorage storage = network.Storages[id];

              if (storage.IsModified)
              {
                this.UpdateStorageRow(storageRow, storage);

                storageRow.set_Value(this.fieldIndices[this.schema.Storages.edited_byColumn], Environment.UserName);
                storageRow.set_Value(this.fieldIndices[this.schema.Storages.edit_reasonColumn], "UPDATED");
                storageRow.set_Value(this.fieldIndices[this.schema.Storages.edit_dateColumn], DateTime.Now);

                cursor.UpdateRow(storageRow);
              }
            }
            else
            {
              storageRow.Delete();
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(storageRow);
          }

          storageRow = cursor.NextRow();
        }

        cursor.Flush();
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      // Add new storages
      if (network.Storages.Count > existingFeatures.Count)
      {
        IRowBuffer storageBuffer = storageTable.CreateRowBuffer();
        cursor = storageTable.Insert(false);
        try
        {
          foreach (IStorage storage in network.Storages)
          {
            if (!existingFeatures.Contains(storage.Id))
            {
              this.UpdateStorageRow(storageBuffer, storage);

              DateTime appendTime = DateTime.Now;
              storageBuffer.set_Value(this.fieldIndices[this.schema.Storages.append_dateColumn], appendTime);
              storageBuffer.set_Value(this.fieldIndices[this.schema.Storages.edit_dateColumn], appendTime);
              storageBuffer.set_Value(this.fieldIndices[this.schema.Storages.edited_byColumn], Environment.UserName);
              storageBuffer.set_Value(this.fieldIndices[this.schema.Storages.edit_reasonColumn], "APPENDED");
            }
          }

          cursor.Flush();
        }
        finally
        {
          ComUtility.ReleaseComObject(cursor);
        }
      }
    }

    private void AuditUpdate(IEntity entity)
    {
      entity.EditDate = DateTime.Now;
      entity.EditedBy = Environment.UserName;
      if (string.IsNullOrWhiteSpace(entity.EditReason))
      {
        entity.EditReason = DefaultEditReason;
      }
    }

    private void AuditInsert(IEntity entity)
    {
      DateTime appendTime = DateTime.Now;
      entity.AppendDate = appendTime;
      entity.EditDate = appendTime;
      entity.EditedBy = Environment.UserName;
      if (string.IsNullOrWhiteSpace(entity.EditReason))
      {
        entity.EditReason = DefaultAppendReason;
      }
    }
    #endregion
    #region Fill Entities

    private void FillNode(INode node, IFeature nodeFeature)
    {
      bool parsed = false;
      node.Geometry = new EsriPoint((ESRI.ArcGIS.Geometry.IPoint)nodeFeature.ShapeCopy);
      node.NodeName = DataUtility.SafeConvertString(nodeFeature.get_Value(this.fieldIndices[this.schema.Nodes.node_nameColumn]));
      node.NodeType = DataUtility.SafeConvertEnum(nodeFeature.get_Value(this.fieldIndices[this.schema.Nodes.node_typeColumn]), NodeType.Manhole, out parsed);
      if (!parsed)
      {
        string message = string.Format("Node record does not have a valid {0}. Default used instead.", this.schema.Nodes.node_typeColumn.ColumnName);
        ValidationFailure invalidNodeType = new ValidationFailure(this.schema.Nodes.node_typeColumn.ColumnName, message);
        invalidNodeType.CustomState = new ValidationState(node, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidNodeType);
      }

      node.GroundElevFt = DataUtility.SafeConvertDouble(nodeFeature.get_Value(this.fieldIndices[this.schema.Nodes.ground_elev_ftColumn]));
      node.TopType = DataUtility.SafeConvertEnum(nodeFeature.get_Value(this.fieldIndices[this.schema.Nodes.top_typeColumn]), NodeTopType.Ponding, out parsed);
      if (!parsed)
      {
        string message = string.Format("Node record does not have a valid {0}. Default used instead.", this.schema.Nodes.top_typeColumn.ColumnName);
        ValidationFailure invalidTopType = new ValidationFailure(this.schema.Nodes.top_typeColumn.ColumnName, message);
        invalidTopType.CustomState = new ValidationState(node, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidTopType);
      }

      node.DiameterFt = DataUtility.SafeConvertDouble(nodeFeature.get_Value(this.fieldIndices[this.schema.Nodes.diameter_ftColumn]), DefaultNodeDiameterFt);

      this.FillAuditProperties(node, nodeFeature, this.schema.Nodes);
    }

    private void FillCrossSection(ICrossSection crossSection, IRow crossSectionRow, ITable dataTable)
    {
      crossSection.CrossSectionName = DataUtility.SafeConvertString(crossSectionRow.get_Value(this.fieldIndices[this.schema.CrossSections.cross_section_nameColumn]));
      if (string.IsNullOrWhiteSpace(crossSection.CrossSectionName))
      {
        string message = string.Format("Cross section (Id = {0}) has an empty name.");
        ValidationFailure invalidCrossSectionName = new ValidationFailure("cross_section_name", message);
        invalidCrossSectionName.CustomState = new ValidationState(crossSection, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidCrossSectionName);
      }

      bool badRatioFound = false;
      IQueryFilter filter = new QueryFilterClass();
      filter.WhereClause = string.Format("{0} = {1}", this.schema.CrossSectionData.cross_section_idColumn.ColumnName, crossSection.Id);
      ICursor cursor = dataTable.Search(filter, false);
      try
      {
        IRow dataRow = cursor.NextRow();
        while (dataRow != null)
        {
          try
          {
            double h = DataUtility.SafeConvertDouble(dataRow.get_Value(this.fieldIndices[this.schema.CrossSectionData.relative_heightColumn]));
            double w = DataUtility.SafeConvertDouble(dataRow.get_Value(this.fieldIndices[this.schema.CrossSectionData.relative_widthColumn]));
            if (!double.IsNaN(h) && !double.IsNaN(w))
            {
              crossSection.AddRatio(h, w);
            }
            else
            {
              badRatioFound = true;
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(dataRow);
          }

          dataRow = cursor.NextRow();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      if (badRatioFound)
      {
        string message = string.Format("Invalid height or width values found in Cross Section Data table for cross section Id {0}. These have been ignored and not loaded.", crossSection.Id);
        ValidationFailure badRatioData = new ValidationFailure(string.Empty, message);
        badRatioData.CustomState = new ValidationState(crossSection, ErrorLevel.Warning);
        this.Validation.Errors.Add(badRatioData);
      }

      this.FillAuditProperties(crossSection, crossSectionRow, this.schema.CrossSections);
    }

    private void FillLink(ILink link, IFeature linkFeature, ITable asBuiltsTable, BusinessClasses.INetwork network)
    {
      bool parsed = false;
      link.Geometry = new EsriPolyline((ESRI.ArcGIS.Geometry.IPolyline)linkFeature.ShapeCopy);
      link.LinkName = DataUtility.SafeConvertInt32(linkFeature.get_Value(this.fieldIndices[this.schema.Links.link_nameColumn]));

      int usNodeId = DataUtility.SafeConvertInt32(linkFeature.get_Value(this.fieldIndices[this.schema.Links.us_node_idColumn]));
      if (network.Nodes.Contains(usNodeId))
      {
        link.USNode = network.Nodes[usNodeId];
      }
      else
      {
        // TODO: Load on demand
        string message = "Link record points to an upstream node that does not exist.";
        ValidationFailure invalidUsNode = new ValidationFailure(this.schema.Links.us_node_idColumn.ColumnName, message, usNodeId);
        invalidUsNode.CustomState = new ValidationState(link, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidUsNode);
      }

      int dsNodeId = DataUtility.SafeConvertInt32(linkFeature.get_Value(this.fieldIndices[this.schema.Links.ds_node_idColumn]));
      if (network.Nodes.Contains(dsNodeId))
      {
        link.DSNode = network.Nodes[dsNodeId];
      }
      else
      {
        // TODO: Load on demand
        string message = "Link record points to a downstream node that does not exist.";
        ValidationFailure invalidDsNode = new ValidationFailure(this.schema.Links.ds_node_idColumn.ColumnName, message, dsNodeId);
        invalidDsNode.CustomState = new ValidationState(link, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidDsNode);
      }

      int crossSectionId = DataUtility.SafeConvertInt32(linkFeature.get_Value(this.fieldIndices[this.schema.Links.cross_section_idColumn]));
      if (network.CrossSections.Contains(crossSectionId))
      {
        link.CrossSection = network.CrossSections[crossSectionId];
      }
      else
      {
        if (crossSectionId != 0)
        {
          // TODO: Load on demand
          string message = "Link record points to a cross section that does not exist.";
          ValidationFailure invalidCrossSection = new ValidationFailure(this.schema.Links.cross_section_idColumn.ColumnName, message, crossSectionId);
          invalidCrossSection.CustomState = new ValidationState(link, ErrorLevel.Warning);
          this.Validation.Errors.Add(invalidCrossSection);
        }
      }

      link.USIEFt = DataUtility.SafeConvertDouble(linkFeature.get_Value(this.fieldIndices[this.schema.Links.us_ie_ftColumn]));
      link.DSIEFt = DataUtility.SafeConvertDouble(linkFeature.get_Value(this.fieldIndices[this.schema.Links.ds_ie_ftColumn]));
      link.Material = DataUtility.SafeConvertEnum(linkFeature.get_Value(this.fieldIndices[this.schema.Links.materialColumn]), Material.Unknown, out parsed);
      if (!parsed)
      {
        string message = "Link record does not have valid material type. Default used instead.";
        ValidationFailure invalidMaterial = new ValidationFailure(this.schema.Links.materialColumn.ColumnName, message);
        invalidMaterial.CustomState = new ValidationState(link, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidMaterial);
      }

      IQueryFilter filter = new QueryFilterClass();
      filter.WhereClause = string.Format("{0} = {1}", this.schema.AsBuilts.link_idColumn.ColumnName, link.Id);
      ICursor cursor = asBuiltsTable.Search(filter, false);
      try
      {
        IRow asBuiltRow = cursor.NextRow();
        while (asBuiltRow != null)
        {
          try
          {
            string asBuilt = DataUtility.SafeConvertString(asBuiltRow.get_Value(this.fieldIndices[this.schema.AsBuilts.asbuilt_nameColumn]));
            if (string.IsNullOrWhiteSpace(asBuilt))
            {
              string message = string.Format("Invalid AsBuilt found for link (id = {0}). The name was null or empty.", link.Id);
              ValidationFailure invalidAsBuilt = new ValidationFailure(this.schema.AsBuilts.asbuilt_nameColumn.ColumnName, message);
              invalidAsBuilt.CustomState = new ValidationState(link, ErrorLevel.Warning);
              this.Validation.Errors.Add(invalidAsBuilt);
            }
            else
            {
              link.AsBuilts.Add(asBuilt);
            }
          }
          finally
          {
            ComUtility.ReleaseComObject(asBuiltRow);
          }

          asBuiltRow = cursor.NextRow();
        }
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }

      link.HeightIn = DataUtility.SafeConvertDouble(linkFeature.get_Value(this.fieldIndices[this.schema.Links.height_inColumn]));
      link.LengthFt = DataUtility.SafeConvertDouble(linkFeature.get_Value(this.fieldIndices[this.schema.Links.length_ftColumn]));

      this.FillAuditProperties(link, linkFeature, this.schema.Links);
    }

    private void FillOrifice(IOrifice orifice, IRow orificeRow, BusinessClasses.INetwork network)
    {
      int usNodeID = DataUtility.SafeConvertInt32(orificeRow.get_Value(this.fieldIndices[this.schema.Orifices.us_node_idColumn]));
      if (network.Nodes.Contains(usNodeID))
      {
        orifice.USNode = network.Nodes[usNodeID];
      }
      else
      {
        // TODO: Load on demand
        string message = "Orifice record points to an upstream node that doesn't exist.";
        ValidationFailure invalidUSNode = new ValidationFailure(this.schema.Orifices.us_node_idColumn.ColumnName, message, usNodeID);
        invalidUSNode.CustomState = new ValidationState(orifice, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidUSNode);
      }

      int toLinkID = DataUtility.SafeConvertInt32(orificeRow.get_Value(this.fieldIndices[this.schema.Orifices.to_link_idColumn]));
      if (network.Orifices.Contains(toLinkID))
      {
        orifice.ToLink = network.Links[toLinkID];
      }
      else
      { 
        if (toLinkID != 0)
        {
          string message = "Orifice record points to a link that does not exist.";
          ValidationFailure invalidToLink = new ValidationFailure(this.schema.Orifices.to_link_idColumn.ColumnName, message, toLinkID);
          invalidToLink.CustomState = new ValidationState(orifice, ErrorLevel.Warning);
          this.Validation.Errors.Add(invalidToLink);
        }
      }

      int dsNodeID = DataUtility.SafeConvertInt32(orificeRow.get_Value(this.fieldIndices[this.schema.Orifices.to_link_idColumn]));
      if (network.Nodes.Contains(dsNodeID))
      {
        orifice.DSNode = network.Nodes[dsNodeID];
      }
      else
      {
        if (dsNodeID != 0)
        {
          string message = "orifice record points to an upstream node that doesn't exist.";
          ValidationFailure invalidDSNode = new ValidationFailure(this.schema.Orifices.ds_node_idColumn.ColumnName, message, dsNodeID);
          invalidDSNode.CustomState = new ValidationState(orifice, ErrorLevel.Warning);
          this.Validation.Errors.Add(invalidDSNode);
        }
      }

      bool orientationParsed = false;
      orifice.Orientation = DataUtility.SafeConvertEnum(orificeRow.get_Value(this.fieldIndices[this.schema.Orifices.orientationColumn]), OrificeOrientation.Side, out orientationParsed);
      if (!orientationParsed)
      {
        string message = "Orifice record does not have a valid orientation. Default used instead.";
        ValidationFailure invalidOrientation = new ValidationFailure(this.schema.Orifices.orientationColumn.ColumnName, message);
        invalidOrientation.CustomState = new ValidationState(orifice, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidOrientation);
      }

      bool shapeParsed = false;
      orifice.OrificeShape = DataUtility.SafeConvertEnum(orificeRow.get_Value(this.fieldIndices[this.schema.Orifices.orifice_shapeColumn]), OrificeShape.Circular, out shapeParsed);
      if (!shapeParsed)
      {
        string message = "Orifice record does not have a valid shape. Default used instead.";
        ValidationFailure invalidShape = new ValidationFailure(this.schema.Orifices.orifice_shapeColumn.ColumnName, message);
        invalidShape.CustomState = new ValidationState(orifice, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidShape);
      }

      orifice.OrificeName = DataUtility.SafeConvertString(orificeRow.get_Value(this.fieldIndices[this.schema.Orifices.orifice_nameColumn]));
      orifice.AreaSqFt = DataUtility.SafeConvertDouble(orificeRow.get_Value(this.fieldIndices[this.schema.Orifices.area_sqftColumn]));
      orifice.Coefficient = DataUtility.SafeConvertDouble(orificeRow.get_Value(this.fieldIndices[this.schema.Orifices.coefficientColumn]));
      orifice.InvertOffsetFt = DataUtility.SafeConvertDouble(orificeRow.get_Value(this.fieldIndices[this.schema.Orifices.invert_offset_ftColumn]));

      this.FillAuditProperties(orifice, orificeRow, this.schema.Orifices);
    }

    private void FillWeir(IWeir weir, IRow weirRow, BusinessClasses.INetwork network)
    {
      bool parsed = false;

      int usNodeId = DataUtility.SafeConvertInt32(weirRow.get_Value(this.fieldIndices[this.schema.Weirs.us_node_idColumn]));
      if (network.Nodes.Contains(usNodeId))
      {
        weir.USNode = network.Nodes[usNodeId];
      }
      else
      {
        // TODO: Load on demand
        string message = "Weir record points to an upstream node that does not exist.";
        ValidationFailure invalidUsNode = new ValidationFailure(this.schema.Weirs.us_node_idColumn.ColumnName, message, usNodeId);
        invalidUsNode.CustomState = new ValidationState(weir, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidUsNode);
      }

      int toLinkId = DataUtility.SafeConvertInt32(weirRow.get_Value(this.fieldIndices[this.schema.Weirs.to_link_idColumn]));
      if (network.Links.Contains(toLinkId))
      {
        weir.ToLink = network.Links[toLinkId];
      }
      else
      {
        if (toLinkId != 0)
        {
          string message = "Weir record points to a link that does not exist.";
          ValidationFailure invalidToLink = new ValidationFailure(this.schema.Weirs.to_link_idColumn.ColumnName, message, toLinkId);
          invalidToLink.CustomState = new ValidationState(weir, ErrorLevel.Warning);
          this.Validation.Errors.Add(invalidToLink);
        }
      }

      int dsNodeId = DataUtility.SafeConvertInt32(weirRow.get_Value(this.fieldIndices[this.schema.Weirs.ds_node_idColumn]));
      if (network.Nodes.Contains(dsNodeId))
      {
        weir.DSNode = network.Nodes[dsNodeId];
      }
      else
      {
        if (dsNodeId != 0)
        {
          string message = "Weir record points to a downstream node that does not exist.";
          ValidationFailure invalidDsNode = new ValidationFailure(this.schema.Weirs.ds_node_idColumn.ColumnName, message, dsNodeId);
          invalidDsNode.CustomState = new ValidationState(weir, ErrorLevel.Warning);
          this.Validation.Errors.Add(invalidDsNode);
        }
      }

      weir.WeirType = DataUtility.SafeConvertEnum(weirRow.get_Value(this.fieldIndices[this.schema.Weirs.weir_typeColumn]), WeirType.Transverse, out parsed);
      if (!parsed)
      {
        string message = "Weir record does not have a valid weir type. Default used instead.";
        ValidationFailure invalidWeirType = new ValidationFailure(this.schema.Weirs.weir_typeColumn.ColumnName, message);
        invalidWeirType.CustomState = new ValidationState(weir, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidWeirType);
      }

      weir.WeirName = DataUtility.SafeConvertString(weirRow.get_Value(this.fieldIndices[this.schema.Weirs.weir_nameColumn]));
      weir.TopElevFt = DataUtility.SafeConvertDouble(weirRow.get_Value(this.fieldIndices[this.schema.Weirs.top_elev_ftColumn]));
      weir.CrestElevFt = DataUtility.SafeConvertDouble(weirRow.get_Value(this.fieldIndices[this.schema.Weirs.crest_elev_ftColumn]));
      weir.LengthFt = DataUtility.SafeConvertDouble(weirRow.get_Value(this.fieldIndices[this.schema.Weirs.length_ftColumn]));
      weir.Coefficient = DataUtility.SafeConvertDouble(weirRow.get_Value(this.fieldIndices[this.schema.Weirs.coefficientColumn]));
      weir.HasFlapGate = DataUtility.SafeConvertBoolean(weirRow.get_Value(this.fieldIndices[this.schema.Weirs.has_flap_gateColumn]));

      this.FillAuditProperties(weir, weirRow, this.schema.Weirs);
    }

    private void FillDirector(IDirector director, IFeature directorFeature, BusinessClasses.INetwork network)
    {
      bool parsed = false;
      director.Geometry = new EsriPoint((ESRI.ArcGIS.Geometry.IPoint)directorFeature.ShapeCopy);

      int areaId = DataUtility.SafeConvertInt32(directorFeature.get_Value(this.fieldIndices[this.schema.Directors.area_idColumn]));
      if (network.Areas.Contains(areaId))
      {
        director.Area = network.Areas[areaId];
      }
      else
      {
        // TODO: Load on demand
        string message = "Director record points to an area that does not exist.";
        ValidationFailure invalidAreaId = new ValidationFailure(this.schema.Directors.area_idColumn.ColumnName, message, areaId);
        invalidAreaId.CustomState = new ValidationState(director, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidAreaId);
      }

      int toNodeId = DataUtility.SafeConvertInt32(directorFeature.get_Value(this.fieldIndices[this.schema.Directors.to_node_idColumn]));
      if (network.Nodes.Contains(toNodeId))
      {
        director.ToNode = network.Nodes[toNodeId];
      }
      else
      {
        // TODO: Load on demand
        if (toNodeId != 0)
        {
          string message = "Director record points to a node that does not exist.";
          ValidationFailure invalidToNode = new ValidationFailure(this.schema.Directors.to_node_idColumn.ColumnName, message, toNodeId);
          invalidToNode.CustomState = new ValidationState(director, ErrorLevel.Warning);
          this.Validation.Errors.Add(invalidToNode);
        }
      }

      director.AreaSqFt = DataUtility.SafeConvertInt32(directorFeature.get_Value(this.fieldIndices[this.schema.Directors.area_sqftColumn]));
      director.DirectorType = DataUtility.SafeConvertEnum(directorFeature.get_Value(this.fieldIndices[this.schema.Directors.director_typeColumn]), DirectorType.Storm, out parsed);
      if (!parsed)
      {
        string message = "Director record does not have a valid director type. Default used instead.";
        ValidationFailure invalidDirectorType = new ValidationFailure(this.schema.Directors.director_typeColumn.ColumnName, message);
        invalidDirectorType.CustomState = new ValidationState(director, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidDirectorType);
      }

      this.FillAuditProperties(director, directorFeature, this.schema.Directors);
    }

    private void FillArea(BusinessClasses.IArea area, IFeature areaFeature, BusinessClasses.INetwork network)
    {
      bool parsed = false;
      area.Geometry = new EsriPolygon((ESRI.ArcGIS.Geometry.IPolygon)areaFeature.ShapeCopy);

      area.AreaType = DataUtility.SafeConvertEnum(areaFeature.get_Value(this.fieldIndices[this.schema.Areas.area_typeColumn]), AreaType.UNKN, out parsed);
      if (!parsed)
      {
        string message = "Area record does not have a valid area type. Default used instead.";
        ValidationFailure invalidAreaType = new ValidationFailure(this.schema.Areas.area_typeColumn.ColumnName, message);
        invalidAreaType.CustomState = new ValidationState(area, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidAreaType);
      }

      area.AreaSqFt = DataUtility.SafeConvertInt32(areaFeature.get_Value(this.fieldIndices[this.schema.Areas.area_sqftColumn]));
      area.SanitaryFlowCfs = DataUtility.SafeConvertDouble(areaFeature.get_Value(this.fieldIndices[this.schema.Areas.sanitary_flow_cfsColumn]));
      area.FlowLengthFt = DataUtility.SafeConvertDouble(areaFeature.get_Value(this.fieldIndices[this.schema.Areas.flow_length_ftColumn]));
      area.SlopeFtPerFt = DataUtility.SafeConvertDouble(areaFeature.get_Value(this.fieldIndices[this.schema.Areas.slope_ft_per_ftColumn]));

      int overlandNodeId = DataUtility.SafeConvertInt32(areaFeature.get_Value(this.fieldIndices[this.schema.Areas.overland_flow_destinationColumn]));
      if (network.Nodes.Contains(overlandNodeId))
      {
        area.OverlandFlowDestination = network.Nodes[overlandNodeId];
      }
      else
      {
        // TODO: Load on demand
        if (overlandNodeId != 0)
        {
          string message = "Area record points to an overland flow destination that does not exist.";
          ValidationFailure invalidFlowDestination = new ValidationFailure(this.schema.Areas.overland_flow_destinationColumn.ColumnName, message, overlandNodeId);
          invalidFlowDestination.CustomState = new ValidationState(area, ErrorLevel.Warning);
          this.Validation.Errors.Add(invalidFlowDestination);
        }
      }

      this.FillAuditProperties(area, areaFeature, this.schema.Areas);
    }

    private void FillInfiltrator(IInfiltrator infiltrator, IRow infiltratorRow, BusinessClasses.INetwork network)
    {
      int nodeID = DataUtility.SafeConvertInt32(infiltratorRow.get_Value(this.fieldIndices[this.schema.Infiltrators.node_idColumn]));
      if (network.Nodes.Contains(nodeID))
      {
        infiltrator.Node = network.Nodes[nodeID];
      }
      else
      {
        // TODO: Load on demand
        string message = "Infiltrator record points to a node that doesn't exist.";
        ValidationFailure invalidNode = new ValidationFailure(this.schema.Infiltrators.node_idColumn.ColumnName, message, nodeID);
        invalidNode.CustomState = new ValidationState(infiltrator, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidNode);
      }

      infiltrator.InfiltratorName = DataUtility.SafeConvertString(infiltratorRow.get_Value(this.fieldIndices[this.schema.Infiltrators.infiltrator_nameColumn]));
      infiltrator.RateInPerHr = DataUtility.SafeConvertDouble(infiltratorRow.get_Value(this.fieldIndices[this.schema.Infiltrators.rate_in_per_hrColumn]));
      infiltrator.AreaSqFt = DataUtility.SafeConvertDouble(infiltratorRow.get_Value(this.fieldIndices[this.schema.Infiltrators.area_sqftColumn]));

      this.FillAuditProperties(infiltrator, infiltratorRow, this.schema.Infiltrators);
    }

    private void FillStorage(IStorage storage, IRow storageRow, BusinessClasses.INetwork network)
    {
      int nodeID = DataUtility.SafeConvertInt32(storageRow.get_Value(this.fieldIndices[this.schema.Storages.node_idColumn]));
      if (network.Nodes.Contains(nodeID))
      {
        storage.Node = network.Nodes[nodeID];
      }
      else
      {
        // TODO: Load on demand
        string message = "Storage record points toa  node that doesn't exist.";
        ValidationFailure invalidNode = new ValidationFailure(this.schema.Storages.node_idColumn.ColumnName, message, nodeID);
        invalidNode.CustomState = new ValidationState(storage, ErrorLevel.Warning);
        this.Validation.Errors.Add(invalidNode);
      }

      storage.StorageName = DataUtility.SafeConvertString(storageRow.get_Value(this.fieldIndices[this.schema.Storages.storage_nameColumn]));
      storage.AreaSqFtPerFtDepth = DataUtility.SafeConvertDouble(storageRow.get_Value(this.fieldIndices[this.schema.Storages.area_sq_ft_per_ft_depthColumn]));
      storage.InvertElevFt = DataUtility.SafeConvertDouble(storageRow.get_Value(this.fieldIndices[this.schema.Storages.invert_elev_ftColumn]));
      
      this.FillAuditProperties(storage, storageRow, this.schema.Storages);
    }

    private void FillAuditProperties(IEntity entity, IRowBuffer entityRow, DataTable schemaTable)
    {
      DataColumn appendDateColumn, editDateColumn, editedByColumn, editReasonColumn;
      this.GetAuditColumns(schemaTable, out appendDateColumn, out editDateColumn, out editedByColumn, out editReasonColumn);

      entity.AppendDate = DataUtility.SafeConvertDateTime(entityRow.get_Value(this.fieldIndices[appendDateColumn]));
      entity.EditDate = DataUtility.SafeConvertDateTime(entityRow.get_Value(this.fieldIndices[editDateColumn]));
      entity.EditedBy = DataUtility.SafeConvertString(entityRow.get_Value(this.fieldIndices[editedByColumn]));
      entity.EditReason = DataUtility.SafeConvertString(entityRow.get_Value(this.fieldIndices[editReasonColumn]));
    }

    #endregion

    #region Update Records

    private void UpdateNodeFeature(IRowBuffer nodeFeature, INode node)
    {
      ESRI.ArcGIS.Geometry.IPoint nodePoint = null;
      if (node.Geometry is EsriPoint)
      {
        nodePoint = ((EsriPoint)node.Geometry).Point;
      }
      else
      {
        nodePoint = new PointClass();
        nodePoint.X = node.Geometry.X;
        nodePoint.Y = node.Geometry.Y;
      }

      if (nodeFeature is IFeature)
      {
        ((IFeature)nodeFeature).Shape = nodePoint;
      }
      else if (nodeFeature is IFeatureBuffer)
      {
        ((IFeatureBuffer)nodeFeature).Shape = nodePoint;
      }

      this.CheckId(node);
      nodeFeature.set_Value(this.fieldIndices[this.schema.Nodes.node_idColumn], node.Id);
      nodeFeature.set_Value(this.fieldIndices[this.schema.Nodes.node_nameColumn], node.NodeName);
      nodeFeature.set_Value(this.fieldIndices[this.schema.Nodes.node_typeColumn], node.NodeType.ToString());
      nodeFeature.set_Value(this.fieldIndices[this.schema.Nodes.ground_elev_ftColumn], node.GroundElevFt);
      nodeFeature.set_Value(this.fieldIndices[this.schema.Nodes.top_typeColumn], node.TopType.ToString());
      nodeFeature.set_Value(this.fieldIndices[this.schema.Nodes.diameter_ftColumn], node.DiameterFt);

      this.UpdateAuditFields(nodeFeature, node, this.schema.Nodes);
    }

    private void UpdateCrossSectionRow(IRowBuffer crossSectionRow, ICrossSection crossSection)
    {
      this.CheckId(crossSection);
      crossSectionRow.set_Value(this.fieldIndices[this.schema.CrossSections.cross_section_idColumn], crossSection.Id);
      crossSectionRow.set_Value(this.fieldIndices[this.schema.CrossSections.cross_section_nameColumn], crossSection.CrossSectionName);

      this.UpdateAuditFields(crossSectionRow, crossSection, this.schema.CrossSections);
    }

    private void DeleteCrossSectionData(ITable dataTable, int id)
    {
      IQueryFilter filter = new QueryFilterClass();
      filter.WhereClause = string.Format("{0} = {1}", this.schema.CrossSectionData.cross_section_idColumn.ColumnName, id);
      dataTable.DeleteSearchedRows(filter);
    }

    private void UpdateCrossSectionData(ITable dataTable, ICrossSection crossSection, int originalId)
    {
      this.DeleteCrossSectionData(dataTable, originalId);
      if (crossSection.Id != originalId)
      {
        this.DeleteCrossSectionData(dataTable, crossSection.Id);
      }

      IRowBuffer buffer = dataTable.CreateRowBuffer();
      ICursor cursor = dataTable.Insert(false);
      try
      {
        foreach (var ratio in crossSection.Ratios)
        {
          buffer.set_Value(this.fieldIndices[this.schema.CrossSectionData.cross_section_idColumn], crossSection.Id);
          buffer.set_Value(this.fieldIndices[this.schema.CrossSectionData.relative_heightColumn], ratio.Key);
          buffer.set_Value(this.fieldIndices[this.schema.CrossSectionData.relative_widthColumn], ratio.Value);

          cursor.InsertRow(buffer);
        }

        cursor.Flush();
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    private void UpdateLinkFeature(IRowBuffer linkFeature, ILink link)
    {
      ESRI.ArcGIS.Geometry.IPolyline linkPolyline = null;
      if (link.Geometry is EsriPolyline)
      {
        linkPolyline = ((EsriPolyline)link.Geometry).Polyline;
      }
      else
      {
        throw new Exception("Link geometry type is not supported. Must be EsriPolyline.");
      }

      if (linkFeature is IFeature)
      {
        ((IFeature)linkFeature).Shape = linkPolyline;
      }
      else if (linkFeature is IFeatureBuffer)
      {
        ((IFeatureBuffer)linkFeature).Shape = linkPolyline;
      }

      // TODO: add link_type, link_flow_type, and dme_global_id to link entity?
      this.CheckId(link);
      linkFeature.set_Value(this.fieldIndices[this.schema.Links.link_idColumn], link.Id);
      linkFeature.set_Value(this.fieldIndices[this.schema.Links.link_nameColumn], link.LinkName);
      linkFeature.set_Value(this.fieldIndices[this.schema.Links.us_node_idColumn], link.USNode == null ? DBNull.Value : (object)link.USNode.Id);
      linkFeature.set_Value(this.fieldIndices[this.schema.Links.ds_node_idColumn], link.DSNode == null ? DBNull.Value : (object)link.DSNode.Id);
      linkFeature.set_Value(this.fieldIndices[this.schema.Links.us_ie_ftColumn], link.USIEFt);
      linkFeature.set_Value(this.fieldIndices[this.schema.Links.ds_ie_ftColumn], link.DSIEFt);
      linkFeature.set_Value(this.fieldIndices[this.schema.Links.materialColumn], link.Material.ToString());
      linkFeature.set_Value(this.fieldIndices[this.schema.Links.cross_section_idColumn], link.CrossSection == null ? 0 : link.CrossSection.Id);
      linkFeature.set_Value(this.fieldIndices[this.schema.Links.height_inColumn], link.HeightIn);
      linkFeature.set_Value(this.fieldIndices[this.schema.Links.length_ftColumn], link.LengthFt);

      this.UpdateAuditFields(linkFeature, link, this.schema.Links);
    }

    private void DeleteAsBuilts(ITable asBuiltsTable, int id)
    {
      IQueryFilter filter = new QueryFilterClass();
      filter.WhereClause = string.Format("{0} = {1}", this.schema.AsBuilts.link_idColumn.ColumnName, id);
      asBuiltsTable.DeleteSearchedRows(filter);
    }

    private void UpdateAsBuilts(ITable asBuiltsTable, ILink link, int originalId)
    {
      this.DeleteAsBuilts(asBuiltsTable, originalId);
      if (link.Id != originalId)
      {
        this.DeleteAsBuilts(asBuiltsTable, link.Id);
      }

      IRowBuffer buffer = asBuiltsTable.CreateRowBuffer();
      ICursor cursor = asBuiltsTable.Insert(false);
      try
      {
        foreach (string asBuilt in link.AsBuilts)
        {
          buffer.set_Value(this.fieldIndices[this.schema.AsBuilts.link_idColumn], link.Id);
          buffer.set_Value(this.fieldIndices[this.schema.AsBuilts.asbuilt_nameColumn], asBuilt);

          cursor.InsertRow(buffer);
        }

        cursor.Flush();
      }
      finally
      {
        ComUtility.ReleaseComObject(cursor);
      }
    }

    private void UpdateOrificeRow(IRowBuffer orificeRow, IOrifice orifice)
    {
      this.CheckId(orifice);
      orificeRow.set_Value(this.fieldIndices[this.schema.Orifices.orifice_idColumn], orifice.Id);
      orificeRow.set_Value(this.fieldIndices[this.schema.Orifices.orifice_nameColumn], orifice.OrificeName);
      orificeRow.set_Value(this.fieldIndices[this.schema.Orifices.us_node_idColumn], orifice.USNode == null ? DBNull.Value : (object)orifice.USNode.Id);
      orificeRow.set_Value(this.fieldIndices[this.schema.Orifices.ds_node_idColumn], orifice.DSNode == null ? DBNull.Value : (object)orifice.DSNode.Id);
      orificeRow.set_Value(this.fieldIndices[this.schema.Orifices.to_link_idColumn], orifice.ToLink == null ? DBNull.Value : (object)orifice.ToLink.Id);
      orificeRow.set_Value(this.fieldIndices[this.schema.Orifices.orientationColumn], orifice.Orientation.ToString());
      orificeRow.set_Value(this.fieldIndices[this.schema.Orifices.orifice_shapeColumn], orifice.OrificeShape.ToString());
      orificeRow.set_Value(this.fieldIndices[this.schema.Orifices.area_sqftColumn], orifice.AreaSqFt);
      orificeRow.set_Value(this.fieldIndices[this.schema.Orifices.coefficientColumn], orifice.Coefficient);
      orificeRow.set_Value(this.fieldIndices[this.schema.Orifices.invert_offset_ftColumn], orifice.InvertOffsetFt);

      this.UpdateAuditFields(orificeRow, orifice, this.schema.Orifices);
    }

    private void UpdateWeirRow(IRowBuffer weirRow, IWeir weir)
    {
      this.CheckId(weir);
      weirRow.set_Value(this.fieldIndices[this.schema.Weirs.weir_idColumn], weir.Id);
      weirRow.set_Value(this.fieldIndices[this.schema.Weirs.weir_nameColumn], weir.WeirName);
      weirRow.set_Value(this.fieldIndices[this.schema.Weirs.us_node_idColumn], weir.USNode == null ? DBNull.Value : (object)weir.USNode.Id);
      weirRow.set_Value(this.fieldIndices[this.schema.Weirs.ds_node_idColumn], weir.DSNode == null ? DBNull.Value : (object)weir.DSNode.Id);
      weirRow.set_Value(this.fieldIndices[this.schema.Weirs.to_link_idColumn], weir.ToLink == null ? DBNull.Value : (object)weir.ToLink.Id);
      weirRow.set_Value(this.fieldIndices[this.schema.Weirs.weir_typeColumn], weir.WeirType.ToString());
      weirRow.set_Value(this.fieldIndices[this.schema.Weirs.top_elev_ftColumn], weir.TopElevFt);
      weirRow.set_Value(this.fieldIndices[this.schema.Weirs.crest_elev_ftColumn], weir.CrestElevFt);
      weirRow.set_Value(this.fieldIndices[this.schema.Weirs.length_ftColumn], weir.LengthFt);
      weirRow.set_Value(this.fieldIndices[this.schema.Weirs.coefficientColumn], weir.Coefficient);
      weirRow.set_Value(this.fieldIndices[this.schema.Weirs.has_flap_gateColumn], weir.HasFlapGate ? (short)1 : (short)0);

      this.UpdateAuditFields(weirRow, weir, this.schema.Weirs);
    }

    private void UpdateInfiltratorRow(IRowBuffer infiltratorRow, IInfiltrator infiltrator)
    {
      this.CheckId(infiltrator);
      infiltratorRow.set_Value(this.fieldIndices[this.schema.Infiltrators.infiltrator_nameColumn], infiltrator.InfiltratorName);
      infiltratorRow.set_Value(this.fieldIndices[this.schema.Infiltrators.infiltrator_idColumn], infiltrator.Id);
      infiltratorRow.set_Value(this.fieldIndices[this.schema.Infiltrators.node_idColumn], infiltrator.Node.Id);
      infiltratorRow.set_Value(this.fieldIndices[this.schema.Infiltrators.rate_in_per_hrColumn], infiltrator.RateInPerHr);
      infiltratorRow.set_Value(this.fieldIndices[this.schema.Infiltrators.area_sqftColumn], infiltrator.AreaSqFt);

      this.UpdateAuditFields(infiltratorRow, infiltrator, this.schema.Infiltrators);
    }

    private void UpdateStorageRow(IRowBuffer storageRow, IStorage storage)
    {
      this.CheckId(storage);
      storageRow.set_Value(this.fieldIndices[this.schema.Storages.storage_nameColumn], storage.StorageName);
      storageRow.set_Value(this.fieldIndices[this.schema.Storages.storage_idColumn], storage.Id);
      storageRow.set_Value(this.fieldIndices[this.schema.Storages.node_idColumn], storage.Node.Id);
      storageRow.set_Value(this.fieldIndices[this.schema.Storages.area_sq_ft_per_ft_depthColumn], storage.AreaSqFtPerFtDepth);
      storageRow.set_Value(this.fieldIndices[this.schema.Storages.invert_elev_ftColumn], storage.InvertElevFt);
    }

    private void UpdateDirectorFeature(IRowBuffer directorFeature, IDirector director, BusinessClasses.INetwork network)
    {
      ESRI.ArcGIS.Geometry.IPoint directorPoint = null;
      if (director.Geometry is EsriPoint)
      {
        directorPoint = ((EsriPoint)director.Geometry).Point;
      }
      else
      {
        directorPoint = new PointClass();
        directorPoint.X = director.Geometry.X;
        directorPoint.Y = director.Geometry.Y;
      }

      if (directorFeature is IFeature)
      {
        ((IFeature)directorFeature).Shape = directorPoint;
      }
      else if (directorFeature is IFeatureBuffer)
      {
        ((IFeatureBuffer)directorFeature).Shape = directorPoint;
      }

      this.CheckId(director);
      directorFeature.set_Value(this.fieldIndices[this.schema.Directors.director_idColumn], director.Id);
      directorFeature.set_Value(this.fieldIndices[this.schema.Directors.area_idColumn], director.Area == null ? DBNull.Value : (object)director.Area.Id);
      directorFeature.set_Value(this.fieldIndices[this.schema.Directors.area_sqftColumn], director.AreaSqFt);
      directorFeature.set_Value(this.fieldIndices[this.schema.Directors.director_typeColumn], director.DirectorType.ToString());
      directorFeature.set_Value(this.fieldIndices[this.schema.Directors.to_node_idColumn], director.ToNode == null ? DBNull.Value : (object)director.ToNode.Id);

      this.UpdateAuditFields(directorFeature, director, this.schema.Directors);
    }

    private void UpdateAreaFeature(IRowBuffer areaFeature, BusinessClasses.IArea area)
    {
      ESRI.ArcGIS.Geometry.IPolygon areaPolygon = null;
      if (area.Geometry is EsriPolygon)
      {
        areaPolygon = ((EsriPolygon)area.Geometry).Polygon;
      }
      else
      {
        throw new Exception("Area geometry type is not supported. Must be EsriPolygon.");
      }

      if (areaFeature is IFeature)
      {
        ((IFeature)areaFeature).Shape = areaPolygon;
      }
      else if (areaFeature is IFeatureBuffer)
      {
        ((IFeatureBuffer)areaFeature).Shape = areaPolygon;
      }

      this.CheckId(area);
      areaFeature.set_Value(this.fieldIndices[this.schema.Areas.area_idColumn], area.Id);
      areaFeature.set_Value(this.fieldIndices[this.schema.Areas.area_typeColumn], area.AreaType.ToString());
      areaFeature.set_Value(this.fieldIndices[this.schema.Areas.area_sqftColumn], area.AreaSqFt);
      areaFeature.set_Value(this.fieldIndices[this.schema.Areas.sanitary_flow_cfsColumn], area.SanitaryFlowCfs);
      areaFeature.set_Value(this.fieldIndices[this.schema.Areas.flow_length_ftColumn], area.FlowLengthFt);
      areaFeature.set_Value(this.fieldIndices[this.schema.Areas.slope_ft_per_ftColumn], area.SlopeFtPerFt);
      areaFeature.set_Value(this.fieldIndices[this.schema.Areas.overland_flow_destinationColumn], area.OverlandFlowDestination == null ? DBNull.Value : (object)area.OverlandFlowDestination.Id);

      this.UpdateAuditFields(areaFeature, area, this.schema.Areas);
    }

    private void CheckId(IEntity entity)
    {
      if (entity != null && entity.Id < 1)
      {
        int nextId;
        if (this.nextIds.TryGetValue(entity.EntityType, out nextId))
        {
          entity.Id = nextId;
          this.nextIds[entity.EntityType] = nextId + 1;
        }
      }
    }

    private void UpdateAuditFields(IRowBuffer entityRow, IEntity entity, DataTable schemaTable)
    {
      DataColumn appendDateColumn, editDateColumn, editedByColumn, editReasonColumn;
      this.GetAuditColumns(schemaTable, out appendDateColumn, out editDateColumn, out editedByColumn, out editReasonColumn);

      entityRow.set_Value(this.fieldIndices[appendDateColumn], entity.AppendDate);
      entityRow.set_Value(this.fieldIndices[editDateColumn], entity.EditDate);
      entityRow.set_Value(this.fieldIndices[editedByColumn], entity.EditedBy);
      entityRow.set_Value(this.fieldIndices[editReasonColumn], entity.EditReason);
    }

    #endregion

    #region Schema Operations

    private void CreateTable(DataTable tableSchema)
    {
      IFeatureWorkspace workspace = this.Workspace as IFeatureWorkspace;

      IObjectClassDescription classDescription = new ObjectClassDescriptionClass();
      IFields fields = classDescription.RequiredFields;
      this.AddSchemaFields(tableSchema, fields);

      UID tableUID = new UIDClass();
      tableUID.Value = "esriGeoDatabase.Object";

      workspace.CreateTable(tableSchema.TableName, fields, tableUID, null, string.Empty);
    }

    private void CreateFeatureClass(DataTable tableSchema, IFeatureDataset featureDataset)
    {
      IObjectClassDescription classDescription = new FeatureClassDescriptionClass();
      IFields fields = classDescription.RequiredFields;

      IField geometryField = null;
      for (int i = 0; i < fields.FieldCount; i++)
      {
        if (fields.get_Field(i).Type == esriFieldType.esriFieldTypeGeometry)
        {
          geometryField = fields.get_Field(i);
          break;
        }
      }

      IGeometryDefEdit geometryDef = (IGeometryDefEdit)geometryField.GeometryDef;
      DataColumn shapeColumn = tableSchema.Columns["Shape"];
      switch (shapeColumn.Caption)
      {
        case "Point":
          geometryDef.GeometryType_2 = esriGeometryType.esriGeometryPoint;
          break;
        case "Polyline":
          geometryDef.GeometryType_2 = esriGeometryType.esriGeometryPolyline;
          break;
        case "Polygon":
          geometryDef.GeometryType_2 = esriGeometryType.esriGeometryPolygon;
          break;
        default:
          throw new NotSupportedException(string.Format("Geometry type for table {0} ('{1}') is not supported.", tableSchema.TableName, shapeColumn.Caption));
      }

      this.AddSchemaFields(tableSchema, fields);

      UID featureUID = new UIDClass();
      featureUID.Value = "esriGeoDatabase.Feature";

      if (featureDataset != null)
      {
        featureDataset.CreateFeatureClass(tableSchema.TableName, fields, featureUID, null, esriFeatureType.esriFTSimple, geometryField.Name, string.Empty);
      }
      else
      {
        IFeatureWorkspace featureWorkspace = this.Workspace as IFeatureWorkspace;
        featureWorkspace.CreateFeatureClass(tableSchema.TableName, fields, featureUID, null, esriFeatureType.esriFTSimple, geometryField.Name, string.Empty);
      }
    }

    private void AddSchemaFields(DataTable tableSchema, IFields fields)
    {
      IFieldsEdit fieldsEdit = (IFieldsEdit)fields;
      foreach (DataColumn column in tableSchema.Columns)
      {
        if (column.ColumnName == "Shape")
        {
          continue;
        }

        IFieldEdit field = new FieldClass();
        field.Name_2 = column.ColumnName;
        field.Type_2 = this.GetEsriColumnType(column.DataType);
        field.IsNullable_2 = column.AllowDBNull;
        if (field.Type == esriFieldType.esriFieldTypeString)
        {
          field.Length_2 = column.MaxLength;
        }

        fieldsEdit.AddField(field);
      }
    }

    private esriFieldType GetEsriColumnType(Type type)
    {
      switch (type.FullName)
      {
        case "System.String":
          return esriFieldType.esriFieldTypeString;
        case "System.Int16":
          return esriFieldType.esriFieldTypeSmallInteger;
        case "System.Int32":
          return esriFieldType.esriFieldTypeInteger;
        case "System.Double":
          return esriFieldType.esriFieldTypeDouble;
        case "System.Single":
          return esriFieldType.esriFieldTypeSingle;
        case "System.DateTime":
          return esriFieldType.esriFieldTypeDate;
        case "System.Guid":
          return esriFieldType.esriFieldTypeGUID;
        case "System.Byte[]":
          return esriFieldType.esriFieldTypeBlob;
        default:
          throw new NotSupportedException("Field type is not supported.");
      }
    }

    private IFeatureClass CheckFeatureClass(IFeatureWorkspace workspace, DataTable featureClassSchema)
    {
      string featureClassName = featureClassSchema.TableName;

      IWorkspace2 workspace2 = workspace as IWorkspace2;
      if (!workspace2.get_NameExists(esriDatasetType.esriDTFeatureClass, featureClassName))
      {
        string message = string.Format("Could not find '{0}' feature class in the database.", featureClassName);
        var error = new ValidationFailure(featureClassName, message);
        error.CustomState = new ValidationState(workspace, ErrorLevel.Error);
        this.Validation.Errors.Add(error);
        this.schemaError = true;
        return null;
      }
      else
      {
        IFeatureClass featureClass = workspace.OpenFeatureClass(featureClassName);

        foreach (DataColumn column in featureClassSchema.Columns)
        {
          this.CheckField(featureClass, column);
        }

        return featureClass;
      }
    }

    private ITable CheckTable(IFeatureWorkspace workspace, DataTable tableSchema)
    {
      string tableName = tableSchema.TableName;

      IWorkspace2 workspace2 = workspace as IWorkspace2;
      if (!workspace2.get_NameExists(esriDatasetType.esriDTTable, tableName))
      {
        string message = string.Format("Could not find the '{0}' table in the database.", tableName);
        var error = new ValidationFailure(tableName, message);
        error.CustomState = new ValidationState(workspace, ErrorLevel.Error);
        this.Validation.Errors.Add(error);
        this.schemaError = true;
        return null;
      }
      else
      {
        ITable table = workspace.OpenTable(tableName);

        foreach (DataColumn column in tableSchema.Columns)
        {
          this.CheckField(table, column);
        }

        return table;
      }
    }

    private void CheckField(IClass table, DataColumn column)
    {
      if (table == null)
      {
        return;
      }

      string fieldName = column.ColumnName;
      int fieldIndex = table.FindField(fieldName);
      if (fieldIndex == -1)
      {
        string classType = "table";
        if (table is IFeatureClass)
        {
          classType = "feature class";
        }

        string message = string.Format("Could not find field '{0}' in the '{1}' {2}.", fieldName, ((IDataset)table).Name, classType);
        var error = new ValidationFailure(fieldName, message);
        error.CustomState = new ValidationState(table, ErrorLevel.Error);
        this.Validation.Errors.Add(error);
        this.schemaError = true;
      }

      this.fieldIndices[column] = fieldIndex;
    }

    private void GetAuditColumns(DataTable schemaTable, out DataColumn appendDateColumn, out DataColumn editDateColumn, out DataColumn editedByColumn, out DataColumn editReasonColumn)
    {
      // We are using the Node schema for the audit field names, as they should be the same across all tables
      appendDateColumn = schemaTable.Columns[this.schema.Nodes.append_dateColumn.ColumnName];
      editDateColumn = schemaTable.Columns[this.schema.Nodes.edit_dateColumn.ColumnName];
      editedByColumn = schemaTable.Columns[this.schema.Nodes.edited_byColumn.ColumnName];
      editReasonColumn = schemaTable.Columns[this.schema.Nodes.edit_reasonColumn.ColumnName];

      if (appendDateColumn == null || editDateColumn == null || editedByColumn == null || editReasonColumn == null)
      {
        throw new DataException(string.Format("The data schema for {0} does not contain correctly named audit fields.", schemaTable.Columns));
      }
    }

    #endregion
  }
}
